/**
 * 
 */
package cz.cuni.mff.abacs.burglar.logics;

import aStarLibrary.AStar;
import aStarLibrary.Node;
import cz.cuni.mff.abacs.burglar.logics.objects.BaseInterface;
import cz.cuni.mff.abacs.burglar.logics.objects.Room;
import cz.cuni.mff.abacs.burglar.logics.objects.agents.*;
import cz.cuni.mff.abacs.burglar.logics.objects.items.Inventory;
import cz.cuni.mff.abacs.burglar.logics.objects.items.Item;
import cz.cuni.mff.abacs.burglar.logics.objects.items.Key;
import cz.cuni.mff.abacs.burglar.logics.objects.items.Uniform;
import cz.cuni.mff.abacs.burglar.logics.objects.positions.*;
import cz.cuni.mff.abacs.burglar.logics.planning.instructions.Instruction;
import cz.cuni.mff.abacs.burglar.visual.VisualBurglar;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;


/** 
 * A game map that executes instructions on it's agents.
 * 
 * @author abacs
 *
 */
public class ExecutingMap extends DataMap implements GameMap {
	// properties:
	
	
	/** List of agents that have no planned action sequence and need to create one. */
	protected List<Agent> _agentsNeedReplan = new LinkedList<Agent>();
	
	
	// -------------------------------------------------------------------------
	// constructors:
	
	
	/**
	 * 
	 * 
	 * @param nextFreeId
	 */
	public ExecutingMap(int nextFreeId, int requiredTrapRoomCount) {
		super(nextFreeId, requiredTrapRoomCount);
	}

	
	// -------------------------------------------------------------------------
	
	
	/**
	 * Returns the full list of A* nodes from the layout.
	 * 
	 */
	public List<Node> getNodes() {
		List<Node> nodes = new LinkedList<Node>();
		for(Position pos : this.getPositions()){
			nodes.add(((BasePosition)pos).getNode());
		}
		return nodes;
	}
	
	
	// -------------------------------------------------------------------------
	
	
	/**
	 * Creates a guard agent and places him to protect the selected rooms.
	 * 
	 * @param roomIds identifiers of the rooms to protect.
	 */
	public void addGuardPatrol(List<Integer> roomIds) {
		Position position = this.getFloorPositions(roomIds.get(0)).get(0);
		Guard guard = new Guard(this.getNextID(), position, roomIds, this);
		
		BeliefBase knowledge = guard.getBeliefBase();
		knowledge.seenFromNear(this.getOperablePositions());
		
		// add the uniform:
		Uniform clothes = new Uniform(this.getNextID(), this);
		guard.addItem(clothes);
		this.addItem(clothes);
		
		
		this.addAgent(guard);
	}
	
	
	// -------------------------------------------------------------------------
	
	
	/**
	 * Adds the instructions to the corresponding agents.
	 * 
	 * The instructions are translated to simple instructions if needed.
	 * 
	 * Unchecked function. Agent code must be correct.
	 */
	@Override
	public void addInstructions(List<Instruction> instructions) {
		// generate simple instructions if needed:
		List<Instruction> simpleInstructions = 
			this.simplifyInstructions(instructions);
		
		// clear the agent instructions that are in the instruction list:
		List<Integer> agents = new LinkedList<Integer>();
		for(Instruction instr : simpleInstructions){
			if(agents.contains(instr._agentId) == false){
				agents.add(instr._agentId);
			}
		}
		for(int agentId : agents)
			this.getAgent(agentId).clearInstructions();
		
		
		// add instructions to their executing agent:
		for(Instruction instr : simpleInstructions){
			assert(this.getAgent(instr._agentId) != null) : "invalid Agent Id in Instruction"; 
			this.getAgent(instr._agentId).addInstruction(instr);
		}
	}
	
	
	@Override
	public void clearInstructions() {
		for(Agent agent : this.getAgents()){
			agent.clearInstructions();
		}
	}
	
	
	@Override
	public List<Instruction> getAgentInstructions(int agentId) {
		return this.getAgent(agentId).getInstructions();
	}
	

	@Override
	public List<Instruction> getAgentInstructions(Agent agent) {
		return agent.getInstructions();
	}
	
	
	@Override
	public boolean instructionListsMatch(
			List<Instruction> a,
			List<Instruction> b
	) {
		if(a.size() != b.size()){
			return false;
		}
		
		for(int i = 0; i < a.size(); i++){
			Instruction instrA = a.get(i);
			Instruction instrB = b.get(i);
			if(instrA.matches(instrB) == false)
				return false;
		}
		
		return true;
	}
	
	
	@Override
	public List<Room> instructionsLeadToTraps(List<Instruction> list) {
		List<Room> trapRooms = this.getTrapRooms();
		int burglarId = this.getBurglar().getId();
		
		List<Room> triggeredRooms = new ArrayList<Room>();
		
		for(Instruction instr : list){
			// if the agent is the burglar and the current room contains a trap
			// the room is added to the triggered trap list
			// all room can be only once in the triggered list.
			
			if(instr._agentId != burglarId)
				continue;
			
			Position position = this.getPosition(instr._subjectId);
			
			if(position.isTypeOf(BaseInterface.Type.DOOR)){
				Door door = (Door)position;
				Room rooms[] = door.getRooms();
				
				if(trapRooms.contains(rooms[0]) &&
				   triggeredRooms.contains(rooms[0]) == false)
					triggeredRooms.add(rooms[0]);
				if(trapRooms.contains(rooms[1]) &&
				   triggeredRooms.contains(rooms[1]) == false)
					triggeredRooms.add(rooms[1]);
			}else{
				Room room = position.getRoom();
				if(trapRooms.contains(room) &&
				   triggeredRooms.contains(room) == false)
					triggeredRooms.add(room);
			}
		}
		
		return triggeredRooms;
	}
	
	
	// -------------------------------------------------------------------------
	
	
	/** 
	 * Executes a single step for each agent.
	 */
	@Override
	public void executeStep() {
		
		for(Agent agent : this.getAgents()){
			
			// only active agents should perform actions:
			if(agent.isInState(Agent.State.WELL) == false)
				continue;
			
			// execute the stepp:
			boolean agentResult = this.executeStep(agent);
			
			// if room contains active vender, go to it:
			GoalBase agentGoals = agent.getGoals();
			if(agentGoals.hasVenderToVisit() == false){
				Position vender = this.getActiveVender(agent.getRoomId());
				if(vender != null){
					agentGoals.setVenderIdToVisit(vender.getId());
					agentResult = false;
				}
			}
			
			// if agent has no more instructions, 
			// or the instruction failed
			// add the agent to the replanning list.
			
			if(agentResult == false || agent.hasInstructions() == false){
				agent.clearInstructions();
				this._agentsNeedReplan.add(agent);
			}
		}
	}
	
	
	/**
	 * Executes the first instruction in the agent's instruction stack.
	 * 
	 * Stunned agents remain immobile.
	 * 
	 * @param agent the agent to act.
	 * @return success indicator.
	 */
	protected boolean executeStep(Agent agent) {
		// if the agent is a stunned guard, do nothing:
		if(agent.isInState(Agent.State.STUNNED)){
			return true;
		}
		
		return this.execute(agent.popFirstInstruction());
	}
	
	
	/** 
	 * Executes a single instruction.
	 * 
	 * @param instruction instruction object to execute
	 * @return success indicator
	 */
	protected boolean execute(Instruction instruction) {
		if(instruction == null)
			return false;
		
		boolean ret = false;
		
		Agent agent = this.getAgent(instruction._agentId);
		assert(agent != null);
		
		switch(instruction._code){
		case MOVE:
			ret = this.move(instruction._agentId, instruction._subjectId);
			break;
		case OPEN:
			ret = this.open(instruction._agentId, instruction._subjectId);
			break;
		case CLOSE:
			ret = this.close(instruction._agentId, instruction._subjectId);
			break;
		case UNLOCK:
			ret = this.unlock(
					instruction._agentId,
					instruction._subjectId,
					instruction._itemId
			);
			break;
		case LOCK:
			ret = this.lock(
					instruction._agentId,
					instruction._subjectId,
					instruction._itemId
			);
			break;
		case PICK_UP:
			ret = this.pickUp(
					instruction._agentId,
					instruction._subjectId,
					instruction._itemId
			);
			break;
		case TAKE_CLOTHES:
			this.takeClothes(
					instruction._agentId,
					instruction._subjectId
			);
			break;
		case USE:
			this.use(instruction._agentId, instruction._subjectId);
			break;
		}
		return ret;
	}
	
	
	/**
	 * Agents look around in their room.
	 * 
	 * They may discover new positions or agents in the room.
	 * If the positions were already known, it does not change them.
	 * 
	 * @return new information indicator
	 */
	@Override
	public boolean lookAround() {
		boolean result = false;
		for(Agent agent : this.getAgents()) {
			boolean agentResult = agent.lookAround();
			result = agentResult || result;
			if(VisualBurglar.FLAG_REPLAN_ON_NEW_KNOWLEDGE && agentResult){
				agent.clearInstructions();
				this._agentsNeedReplan.add(agent);
			}
		}
		return result;
	}
	
	// -------------------------------------------------------------------------
	
	
	/** Returns whether the map has agents that need replanning. */
	@Override
	public boolean needsReplanning() {
		return ! this._agentsNeedReplan.isEmpty();
	}
	
	
	/**
	 * Returns the list of agents that require replanning.
	 * 
	 * @return 
	 */
	@Override
	public List<Agent> getAgentsToReplan() {
		return new ArrayList<Agent>(this._agentsNeedReplan);
	}
	
	
	@Override
	public void clearAgentsToReplan() {
		this._agentsNeedReplan.clear();
	}
	
	
	@Override
	public void addAgentToReplan(Agent agent) {
		this._agentsNeedReplan.add(agent);
	}
	
	
	@Override
	public void removeAgentToReplan(Agent agent) {
		this._agentsNeedReplan.remove(agent);
	}
	
	
	// -------------------------------------------------------------------------
	
	
	@Override
	public List<Position> nodesToPositions(List<Node> nodes) {
		List<Position> positions = new ArrayList<Position>();
		for(Node node : nodes)
			positions.add(this.getPosition(node));
		return positions;
	}
	
	
	// -------------------------------------------------------------------------
	
	
	/**
	 * Replaces a complex instruction with simple ones. 
	 * 
	 * Used to break apart movement instructions.
	 * 
	 * Destructive operation on the agent parameter.
	 * @param testAgent a test copy of the agent that will execute the instructions.
	 * @param instruction instruction to process.
	 * @return resulted instruction list
	 */
	protected List<Instruction> simplifyInstruction(
			Instruction instruction,
			Agent testAgent
	) {
		List<Instruction> result = new ArrayList<Instruction>();
		
		Instruction simpleInstr;
		
		switch(instruction._code){
			case ENTER_DOOR:
				simpleInstr = 
					new Instruction(
							Instruction.code.MOVE,
							instruction._agentId,
							instruction._subjectId
					);
				result.add(simpleInstr);
				testAgent.setPosition(instruction._subjectId);
				break;
				
			case COMPLEX_MOVE:
				Position subjectPos = this.getPosition(instruction._subjectId);
				if(subjectPos == null)
					subjectPos = this.getAgent(instruction._subjectId).getPosition();
				List<Position> path = 
						this.getSimplePath(
							testAgent.getPosition(),
							subjectPos
						);
				
				for(Position position : path){
					simpleInstr = 
						new Instruction(
								Instruction.code.MOVE,
								instruction._agentId,
								position.getId()
						);
					result.add(simpleInstr);
					testAgent.setPosition(position);
				}
				break;
				
			default:
				result.add(instruction);
		}
		
		return result;
	}
	
	
	/**
	 * Replaces complex instructions with simple ones.
	 * 
	 * Instructions have to contain a single agent.
	 * 
	 * @param instructions list of instructions to process.
	 * @return resulted instruction list.
	 */
	protected List<Instruction> simplifyInstructions(
			List<Instruction> instructions
	) {
		List<Instruction> result = new ArrayList<Instruction>();
		
		if(instructions.isEmpty())
			return result;
		
		// creates a test copy that will be destroyed later;
		Agent testAgent = this.getAgent(instructions.get(0)._agentId).copy(this);
		
		for(Instruction instr : instructions){
				result.addAll(this.simplifyInstruction(instr, testAgent));
		}
		
		return result;
	}
	
	
	/**
	 * Generates a path between two positions.
	 * 
	 * The result does not include the start, neither
	 * the end position. It does not calculate with doors, but 
	 * it does use only walkable positions.
	 * 
	 * @param from starting position
	 * @param to movement aim
	 * @return list of positions from start to aim in correct order.
	 */
	protected List<Position> getSimplePath(Position from, Position to) {
		List<Position> result = new ArrayList<Position>();
		
		if(from.getId() == to.getId())
			return result;
		
		// plan the path:
		AStar aStar = new AStar();
		aStar.nodes.addAll(this.getNodes());
		
		List<Node> nodes = 
			aStar.getPath(
					((BasePosition)from).getNode(),
					((BasePosition)to).getNode()
			);
		
		// the a* algorithm returns the nodes in oposite order
		// we only need the last stepp if it's not the target position
		for(int index = nodes.size() - 2; index > 0; index--){
			result.add(this.getPosition(nodes.get(index)));
		}
		
		Position lastPosition = this.getPosition(nodes.get(0));
		if(lastPosition.getId() != to.getId()){
			result.add(lastPosition);
		}
		
		return result;
	}
	
	// -------------------------------------------------------------------------
	
	
	@Override
	public boolean isBurglarTrapped() {
		Burglar burglar = this.getBurglar();
		
		// burglar can hide from cameras
		if(burglar.isDisguised() == false){
			for(Position pos : this.getTrapPositions()){
				if(pos.isTypeOf(BaseInterface.Type.CAMERA)){
					Camera camera = (Camera)pos;
					if(
						camera.isActive() &&
						burglar.isInRoom(camera.getRoomId())
					)
						return true;
				}
			}
		}
		
		for(Guard guard : this.getGuards()){
			if(
				guard.isInState(Guard.State.WELL) &&
				guard.getRoomId() == burglar.getRoomId()
			)
				return true;
		}
		return false;
	}
	
	
	// -------------------------------------------------------------------------
	// instructions:
	
	
	/**
	 * Executes a directional move instruction.
	 * 
	 * @param agentId acting agent
	 * @param direction movement direction
	 * @return success of the operation
	 */
	public boolean move(int agentId, Direction direction) {
		return this.move(this.getAgent(agentId), direction);
	}
	
	
	/**
	 * Executes a directional move instruction.
	 * 
	 * @param agent acting agent
	 * @param direction movement direction
	 * @return success of the operation
	 */
	boolean move(Agent agent, Direction direction) {
		Position aim = this.getNeighbour(agent.getPosition(), direction);
		if(aim == null)
			return false;
		return agent.moveTo(aim);
	}
	
	
	/**
	 * Executes a simple move instruction.
	 * 
	 * @param agentId acting agent
	 * @param walkableId walkable position to step on
	 * @return success of the operation
	 */
	boolean move(int agentId, int walkableId) {
		return this.move(this.getAgent(agentId), this.getPosition(walkableId));
	}
	
	
	/**
	 * Executes a simple move instruction.
	 * 
	 * @param agent acting agent
	 * @param walkable walkable position to step on
	 * @return success of the operation
	 */
	private boolean move(Agent agent, Position walkable) {
		return agent.moveTo(walkable);
	}
	
	
	/**
	 * Executes an open instruction.
	 * 
	 * @param agentId acting agent
	 * @param lockableId lockable position to open
	 * @return success of the operation
	 */
	private boolean open(int agentId, int lockableId) {
		return this.open(
				this.getAgent(agentId),
				(Lockable)this.getPosition(lockableId)
		);
	}
	
	
	/**
	 * Executes an open instruction.
	 * 
	 * @param agent acting agent
	 * @param lockable lockable position to open
	 * @return success of the operation
	 */
	private boolean open(Agent agent, Lockable lockable) {
		return agent.open(lockable);
	}
	
	
	/**
	 * Executes a close instruction.
	 * 
	 * @param agentId acting agent
	 * @param lockableId lockable position to close
	 * @return success of the operation
	 */
	private boolean close(int agentId, int lockableId) {
		return this.close(
				this.getAgent(agentId),
				(Lockable) this.getPosition(lockableId)
		);
	}
	
	
	/**
	 * Executes a close instruction.
	 * 
	 * @param agent acting agent
	 * @param lockable lockable position to close
	 * @return success of the operation
	 */
	private boolean close(Agent agent, Lockable lockable) {
		return agent.close(lockable);
	}
	
	
	/**
	 * Executes an unlock instruction.
	 * 
	 * @param agentId acting agent
	 * @param lockableId lockable position to unlock
	 * @param keyId key to use
	 * @return success of the operation
	 */
	private boolean unlock(int agentId, int lockableId, int keyId) {
		return this.unlock(
				this.getAgent(agentId),
				(Lockable) this.getPosition(lockableId),
				(Key) this.getItem(keyId)
		);
	}
	
	
	/**
	 * Executes an unlock instruction.
	 * 
	 * @param agent acting agent
	 * @param lockable lockable position to unlock
	 * @param key key to use
	 * @return success of the operation
	 */
	private boolean unlock(Agent agent, Lockable lockable, Key key) {
		return agent.unlock(lockable, key);
	}
	
	
	/**
	 * Executes a lock instruction.
	 * 
	 * @param agentId acting agent
	 * @param lockableId lockable position to lock
	 * @param keyId key to use
	 * @return success of the operation
	 */
	private boolean lock(int agentId, int lockableId, int keyId) {
		return this.lock(
				this.getAgent(agentId),
				(Lockable) this.getPosition(lockableId),
				(Key) this.getItem(keyId)
		);
	}
	
	
	/**
	 * Executes a lock instruction.
	 * 
	 * @param agent acting agent
	 * @param lockable lockable position to lock
	 * @param key key to use
	 * @return success of the operation
	 */
	private boolean lock(Agent agent, Lockable lockable, Key key) {
		return agent.lock(lockable, key);
	}
	
	
	/**
	 * Executes a pick up instruction.
	 * 
	 * @param agentId acting agent
	 * @param inventoryId from where to pick up the object
	 * @param itemId item to pick up
	 * @return success of the operation
	 */
	private boolean pickUp(int agentId, int inventoryId, int itemId) {
		return this.pickUp(
				this.getAgent(agentId),
				(Inventory) this.getPosition(inventoryId),
				this.getItem(itemId)
		);
	}
	
	
	/**
	 * Executes a pick up instruction.
	 * 
	 * @param agent acting agent
	 * @param inventory from where to pick up the object
	 * @param item item to pick up
	 * @return success of the operation
	 */
	private boolean pickUp(Agent agent, Inventory inventory, Item item) {
		return agent.pickUp(inventory, item);
	}
	
	
	/**
	 * Executes a take clothes instruction.
	 * 
	 * @param agentId acting agent
	 * @param subjectAgentId from where to pick up the clothes
	 * @return success of the operation
	 */
	private boolean takeClothes(int agentId, int subjectAgentId) {
		return this.takeClothes(
				this.getAgent(agentId),
				this.getAgent(subjectAgentId)
		);
	}
	
	
	/**
	 * Executes a take clothes instruction.
	 * 
	 * @param agent acting agent
	 * @param subjectAgent from where to pick up the clothes
	 * @return success of the operation
	 */
	private boolean takeClothes(Agent agent, Agent subjectAgent) {
		int id = subjectAgent.getItemIdOfType(BaseInterface.Type.UNIFORM);
		
		Item item;
		if(id != -1)
			item = subjectAgent.removeItem(id);
		else{
			item = new Uniform(this.getNextID(), this);
			this.addItem(item);
		}
		agent.addItem(item);
		
		System.out.println(
			"- " + agent.getId() + 
			": Clothes taken from " + subjectAgent.getId()
		);
		return true;
	}
	
	
	/**
	 * Executes a use instruction.
	 * 
	 * @param agentId acting agent
	 * @param venderId vender position to use
	 * @return success of the operation
	 */
	private boolean use(int agentId, int venderId) {
		return this.use(
				this.getAgent(agentId),
				(Vender) this.getPosition(venderId)
		);
	}
	
	
	/**
	 * Executes a use instruction.
	 * 
	 * @param agent acting agent
	 * @param lockable vender position to use
	 * @return success of the operation
	 */
	private boolean use(Agent agent, Vender vender) {
		return agent.use(vender);
	}
	
}
